﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq.Expressions;
using System.Reflection;

namespace Loki.Model
{
    public class EntityContainer<TEntityClass, TEntityKey> : LokiObject, IDisposable, IContainer<TEntityClass>, IIndexedEnumerable<TEntityClass, TEntityKey>
        where TEntityClass : IAtomicEntity<TEntityKey>
    {
        #region Disposable
        /// <summary>
        /// Releases all resources used by an instance of the <see cref="EntityContainer" /> class.
        /// </summary>
        /// <remarks>
        /// This method calls the virtual <see cref="Dispose(bool)" /> method, passing in <strong>true</strong>, and then suppresses 
        /// finalization of the instance.
        /// </remarks>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Releases unmanaged resources before an instance of the <see cref="EntityContainer" /> class is reclaimed by garbage collection.
        /// </summary>
        /// <remarks>
        /// This method releases unmanaged resources by calling the virtual <see cref="Dispose(bool)" /> method, passing in <strong>false</strong>.
        /// </remarks>
        ~EntityContainer()
        {
            Dispose(false);
        }

        /// <summary>
        /// Releases the unmanaged resources used by an instance of the <see cref="EntityContainer" /> class and optionally releases the managed resources.
        /// </summary>
        /// <param name="disposing"><strong>true</strong> to release both managed and unmanaged resources; <strong>false</strong> to release only unmanaged resources.</param>
        protected virtual void Dispose(bool disposing)
        {

        }
        #endregion

        #region Fields
        private object _Lock = new object();
        private Dictionary<string, Func<object, IEnumerable<TEntityClass>>> _IndexLoaders = new Dictionary<string, Func<object, IEnumerable<TEntityClass>>>();
        private Func<TEntityKey, TEntityClass> _PKLoader;
        private Func<IEnumerable<TEntityClass>> _GlobalLoader;
        private Func<TEntityKey, TEntityClass> _DefaultGetter;
        private ContainerConfiguration<TEntityClass, TEntityKey> _Config;
        private bool _Loaded = false;
        private Dictionary<string, IKeyMapper<TEntityClass>> _Indexes = new Dictionary<string, IKeyMapper<TEntityClass>>();
        private UniqueKeyMapper<TEntityClass, TEntityKey> _PK;
        #endregion

        public EntityContainer(ContainerConfiguration<TEntityClass, TEntityKey> P_Configuration)
        {
            _Config = P_Configuration;

            // configure primary key
            _PK = new UniqueKeyMapper<TEntityClass, TEntityKey>(_Config.PrimaryKey.Indexer, _Config.PrimaryKey.Comparer);
            _PKLoader = _Config.PrimaryKey.Loader;

            _GlobalLoader = _Config.FullLoader.Compile();
            if (_Config.PrimaryKey.Default != null)
                _DefaultGetter = _Config.PrimaryKey.Default;

            // other indexes
            if (_Config.Indexes != null)
            {
                // configure indexes
                foreach (IIndexConfiguration<TEntityClass> L_IndexConfig in _Config.Indexes)
                {
                    PropertyInfo L_IndexerInfo = ExpressionHelper.Reflect<TEntityClass>.GetProperty(L_IndexConfig.Indexer);
                    string L_PropertyName = L_IndexerInfo.Name;
                    Type L_FKType = L_IndexerInfo.PropertyType;
                    IEqualityComparer<TEntityClass> L_Comparer = null;
                    if (typeof(TEntityClass).GetInterface(typeof(IAtomicEntity<TEntityKey>).Name) != null)
                    {
                        L_Comparer = new EntityEqualityComparer<TEntityClass, TEntityKey>();
                    }

                    Type L_MapperType = typeof(IndexedKeyMapper<,>).MakeGenericType(new System.Type[] { typeof(TEntityClass), L_FKType });

                    _Indexes[L_PropertyName] = (IKeyMapper<TEntityClass>)Activator.CreateInstance(L_MapperType, L_IndexConfig, L_Comparer);

                    // register dedicated loader
                    if (L_IndexConfig.Loader != null)
                    {
                        _IndexLoaders[L_PropertyName] = L_IndexConfig.Loader;
                    }
                }
            }
        }

        #region Global load and remove
        public void FullLoad()
        {
            lock (_Lock)
            {
                _EnsureFullLoad();
            }
        }

        public void Reset()
        {
            lock (_Lock)
            {
                _Reset();
            }
        }

        private static NotifyCollectionChangedEventArgs _ResetArgs = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);

        private void _Reset()
        {
            _PK.Reset();
            foreach (IKeyMapper<TEntityClass> L_Mapper in _Indexes.Values)
            {
                L_Mapper.Reset();
            }
            _Loaded = false;

            OnCollectionChanged(_ResetArgs);
        }
        #endregion

        #region Getters
        public TEntityClass this[TEntityKey P_Index]
        {
            get { return this.Get(P_Index); }
        }

        public IEnumerable<TEntityClass> this[string P_FKName, object P_FK]
        {
            get { return GetAllBy(P_FKName, P_FK); }
        }

        /// <summary>
        /// Gets the <see cref="System.Collections.Generic.IEnumerable&lt;TEntityClass&gt;"/> with the specified value on the specified member.
        /// </summary>
        /// <param name="P_FKSelector">The FK selector.</param>
        /// <param name="P_FK">The member value</param>
        /// <returns></returns>
        public IEnumerable<TEntityClass> GetAllBy(string P_FKName, object P_FK)
        {
            lock (_Lock)
            {
                if (_Indexes.ContainsKey(P_FKName))
                {
                    _EnsureFKLoaded(P_FKName, P_FK);

                    foreach (var L_Item in _Indexes[P_FKName].GetAllByKey(P_FK))
                    {
                        if (Object.Equals(_Purify(L_Item), L_Item))
                        {
                            yield return L_Item;
                        }
                    }
                }
                else
                {
                    throw BuildErrorFormat(ToolsError.TOO_120, P_FKName, typeof(TEntityClass).Name);
                    /*Log.Error(Ex.Code, Ex);
                    Func<TEntityClass, object> L_FKSelector = P_FKSelector.Compile();
                    _EnsureFullLoad();
                    return _PK.Where(x => Object.Equals(L_FKSelector(x), P_FK) && !Object.Equals(_Purify(x), default(TEntityClass)));*/
                }
            }
        }

        /// <summary>
        /// Gets the entity with the specified primary key.
        /// </summary>
        /// <param name="P_Key">The key</param>
        /// <returns></returns>
        public TEntityClass Get(TEntityKey P_Key)
        {
            lock (_Lock)
            {
                _EnsurePKLoaded(P_Key, true);
                return _Purify(_PK.Get(P_Key));
            }
        }
        #endregion

        #region Registering
        /// <summary>
        /// Registers the specified entity.
        /// </summary>
        /// <param name="P_Entity">The entity</param>
        public bool Register(TEntityClass P_Entity)
        {
            lock (_Lock)
            {
                return A_AddOrReplaceItem(P_Entity);
            }
        }

        private bool A_AddOrReplaceItem(TEntityClass P_Entity)
        {
            bool L_AlreadyPresent;
            NotifyCollectionChangedEventArgs e = null;

            L_AlreadyPresent = _IsLoaded(P_Entity);
            if (L_AlreadyPresent)
            {
                e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, P_Entity, _PK.Get(GetKey(P_Entity)));
            }
            else
            {
                e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, P_Entity);
            }

            // registering
            _IndexItem(P_Entity);

            OnCollectionChanged(e);
            return !L_AlreadyPresent;
        }

        private void _IndexItem(TEntityClass P_Entity)
        {
            _PK.Register(P_Entity);
            foreach (IKeyMapper<TEntityClass> L_Index in _Indexes.Values)
            {
                L_Index.Register(P_Entity);
            }
        }

        private bool _IsLoaded(TEntityClass P_Entity)
        {
            TEntityKey L_Key = (TEntityKey)_PK.ComputeIndex(P_Entity);
            return _PK.IsLoaded(L_Key) && Object.Equals(_Purify(P_Entity), P_Entity);
        }

        private TEntityKey GetKey(TEntityClass P_Entity)
        {
            return (TEntityKey)_PK.ComputeIndex(P_Entity);
        }

        public void Remove(TEntityClass P_Entity)
        {
            lock (_Lock)
            {
                _Remove(P_Entity);
            }
        }

        private void _Remove(TEntityClass P_Entity)
        {
            TEntityKey L_Key = (TEntityKey)_PK.ComputeIndex(P_Entity);

            // remove
            _PK.Remove(P_Entity);
            foreach (IKeyMapper<TEntityClass> L_Index in _Indexes.Values)
            {
                L_Index.Remove(P_Entity);
            }

            Type L_Type = typeof(TEntityClass);
            Log.InfoFormat("Removing entity {0} of type {1}", L_Key, L_Type.Name);
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, P_Entity));

        }
        #endregion

        #region Loading checks
        public void InvalidateKey(Expression<Func<TEntityClass, object>> P_FKSelector, object P_FkValue)
        {
            lock (_Lock)
            {
                string L_FkName = ExpressionHelper.Reflect<TEntityClass>.GetProperty(P_FKSelector).Name;
                Log.DebugFormat("Invalidated {0} {1} on {2} container", L_FkName, P_FkValue, typeof(TEntityClass).Name);
                _Indexes[L_FkName].Invalidate(P_FkValue);

                _Loaded = false;
            }
        }
        /// <summary>
        /// Checks (and load if not) if the entity with the specified key is loaded.
        /// </summary>
        /// <param name="P_Key">The key</param>
        /// <param name="P_RaiseNotFoundError">if set to <c>true</c> [p_ raise not found error].</param>
        private TEntityClass _EnsurePKLoaded(TEntityKey P_Key, bool P_RaiseNotFoundError)
        {
            TEntityClass L_Return = _PK.Get(P_Key);
            if (!object.Equals(L_Return, default(TEntityClass)))
                L_Return = _Purify(L_Return);
            if (object.Equals(L_Return, default(TEntityClass)))
            {
                // entity not registerd, try loading
                L_Return = default(TEntityClass);
                try
                {
                    L_Return = _PKLoader(P_Key);
                }
                catch (Exception ex)
                {
                    throw BuildError(_Config.LoadingErrorCode, ex);
                }

                // check if entity is found
                if (!object.Equals(L_Return, default(TEntityClass)))
                {
                    A_AddOrReplaceItem(L_Return);
                }
                // not found error if specified
                else if (_Config.PrimaryKey.NotFoundErrorCode != null & P_RaiseNotFoundError)
                {
                    throw BuildErrorFormat(_Config.PrimaryKey.NotFoundErrorCode, P_Key);
                }
                // load default value for not recurrent calls
                else if (_DefaultGetter != null)
                {
                    A_AddOrReplaceItem(_DefaultGetter(P_Key));
                }
            }

            return L_Return;
        }

        /// <summary>
        /// Check or load all.
        /// </summary>
        private void _EnsureFullLoad()
        {
            // global load if not loaded
            if (!_Loaded)
            {
                foreach (var L_Index in _Indexes.Values)
                {
                    L_Index.Reset();
                }

                foreach (TEntityClass L_Entity in _GlobalLoader())
                {
                    _IndexItem(L_Entity);
                }

                _Loaded = true;
                OnCollectionChanged(_ResetArgs);
            }
        }

        /// <summary>
        /// Checks (and load if not) if the entities with the specified value on the specified member are loaded.
        /// </summary>
        /// <param name="P_FKName">The FK name</param>
        /// <param name="P_Fkvalue">The fkvalue</param>
        private void _EnsureFKLoaded(string P_FKName, object P_Fkvalue)
        {
            // check if the container is full loaded
            if (!(_Loaded || _Indexes[P_FKName].IsLoaded(P_Fkvalue)))
            {
                // check if a the key is loaded and if a specific loader is available
                if (_IndexLoaders.ContainsKey(P_FKName) && !_Indexes[P_FKName].IsLoaded(P_Fkvalue))
                {
                    try
                    {
                        IEnumerable<TEntityClass> _Entities = _IndexLoaders[P_FKName](P_Fkvalue);
                        foreach (TEntityClass L_Entity in _Entities)
                        {
                            _IndexItem(L_Entity);
                        }
                        Log.DebugFormat("Validated {0} {1} on {2} container", P_FKName, P_Fkvalue, typeof(TEntityClass).Name);
                        _Indexes[P_FKName].MarkAsLoaded(P_Fkvalue);

                        OnCollectionChanged(_ResetArgs);
                    }
                    catch (Exception ex)
                    {
                        throw BuildError(_Config.LoadingErrorCode, ex);
                    }
                }
                else
                {
                    // no specific loader defined
                    _EnsureFullLoad();
                }
            }
        }

        private TEntityClass _Purify(TEntityClass P_Class)
        {
            if (!object.Equals(P_Class, default(TEntityClass)))
            {
                foreach (var L_Index in _Indexes.Values)
                {
                    object L_IndexValue = L_Index.ComputeIndex(P_Class);
                    if (L_Index.IsInvalidated(L_IndexValue))
                        return default(TEntityClass);
                }
            }
            return P_Class;
        }
        #endregion

        #region IEnumerable
        /// <summary>
        /// Retourne un énumérateur qui parcourt la collection.
        /// </summary>
        /// <returns>
        ///   <see cref="T:System.Collections.Generic.IEnumerator`1"/> pouvant être utilisé pour parcourir la collection.
        /// </returns>
        public IEnumerator<TEntityClass> GetEnumerator()
        {
            lock (_Lock)
            {
                _EnsureFullLoad();
                foreach (TEntityClass L_Item in _PK)
                {
                    if (Object.Equals(_Purify(L_Item), L_Item))
                        yield return L_Item;
                }
            }
        }

        /// <summary>
        /// Retourne un énumérateur qui itère au sein d'une collection.
        /// </summary>
        /// <returns>
        /// Objet <see cref="T:System.Collections.IEnumerator"/> pouvant être utilisé pour itérer au sein de la collection.
        /// </returns>
        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }
        #endregion

        #region Events
        #region Collection Changed
        private readonly object CollectionChangedEventLock = new object();
        private NotifyCollectionChangedEventHandler CollectionChangedEvent;

        public event NotifyCollectionChangedEventHandler CollectionChanged
        {
            add
            {
                lock (CollectionChangedEventLock)
                {
                    CollectionChangedEvent += value;
                }
            }
            remove
            {
                lock (CollectionChangedEventLock)
                {
                    CollectionChangedEvent -= value;
                }
            }
        }

        protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
        {
            NotifyCollectionChangedEventHandler handler = null;

            lock (CollectionChangedEventLock)
            {
                handler = CollectionChangedEvent;

                if (handler == null)
                    return;
            }

            handler(this, e);
        }

        #endregion
        #endregion
    }
}
